package model;

import model.interfaces.selectable.SelectableRallyPoint;
import model.interfaces.selectable.SelectableInstance;
import model.interfaces.selectable.SelectableCommand;
import model.interfaces.selectable.InstanceTypeAndHealth;

import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.ListIterator;
import java.util.Map;

/**
 * 
 * @author Chris
 * 
 */
class RallyPoint implements SelectableRallyPoint, SelectableInstance, ModelInstance
{
    /*
     * Note: combat is an ANTI-GOAL in this iteration. Don't worry about
     * implementing it. focus on getting units and rally points to MOVE! :-)
     */
    // note:
    // Controller wants list of selectableInstances
    // wants:
    // getAllUnits
    // getReinforcemts
    // get BattleGroup
    // perhaps put these in interface SelectableRallyPoint?
    
    // attributes
    private LinkedList< Unit > units;
    private LinkedList< Unit > battle_group;
    private LinkedList< Unit > reinforcements;
    private Type tunits;
    private Type tbattle_group;
    private Type treinforcements;
    private LinkedList< Command > command_queue;
    private boolean attacking;
    private boolean defending;
    private Direction direction_facing;
    
    private static LinkedList< SelectableCommand > avail_commands;
    
    // initialize possible Rally Point commands
    {
        avail_commands = new LinkedList< SelectableCommand >();
        avail_commands.add( new Attack() );
        avail_commands.add( new Defend() );
        avail_commands.add( new Move() );
        avail_commands.add( new Wait() );
        avail_commands.add( new Decommission() );
        avail_commands.add( new DisbandArmy() );
        avail_commands.add( new PowerUp() );
        avail_commands.add( new PowerDown() );
    }
    
    private int id; // RallyPoint's ID number
    private int stepcount;
    private int max_speed;
    
    /**
     * Creates a new RallyPoint with no units associated with it and the
     * specified ID number. The command queue is empty, the RallyPoint is not
     * attacking or defending, and the default heading is north.
     * 
     * @author Christopher Dudley
     * @author Chris Woolfe
     * 
     * @param id
     *            the ID number of the RallyPoint.
     */
    public RallyPoint( int id ) {
        units = new LinkedList< Unit >();
        battle_group = new LinkedList< Unit >();
        reinforcements = new LinkedList< Unit >();
        command_queue = new LinkedList< Command >();
        attacking = false;
        defending = false;
        direction_facing = Direction.N;
        this.id = id;
        max_speed = 0;
        stepcount = 0;
        tunits = new Type( "error" ); // will be overritten by storeTypes method
        tbattle_group = new Type( "error" );
        treinforcements = new Type( "error" );
        command_queue.add( new Wait() );
        
    }
    
    // public methods
    /**
     * 
     * used for attacking. player class calls this method. damage is the amount
     * of pain that will be dealt to the unit. direction is the direction the
     * attack is coming from. if the damage is coming from a direction that the
     * unit is defending against (direction = East and this is defending against
     * the West) then the defensive damage (defensepower) will be returned. if
     * this is not defending then the method shall return zero. if this is
     * defending in the wrong direction then the method shall return zero.
     */
    public int takeDamage( int damage, Direction direction )
    {
        // pick random unit in battle group and apply to that.
        Random r = new Random();
        int index = r.nextInt( battle_group.size() );
        battle_group.get( index ).takeDamage( damage );
        
        if ( direction.getOpposite() == direction_facing )
            return battle_group.get( index ).getDefensePower();
        else
            return 0;
        
    }
    
    /**
     * 
     * called by player this will set the direction the rally point is defending
     * against. that is all.
     */
    public void defend( Direction direction )
    {
        direction_facing = direction;
        defending = true;
        attacking = false;
    }
    
    /**
     * 
     * called by player this will get the direction rally point is facing.
     */
    public Direction getDirection()
    {
        return direction_facing;
    }
    
    /**
     * 
     * called by player this will return an array of the offensive damages
     * (offensivepower) of all the units in the army's battle group
     */
    public int[] getDamages()
    {
        int[] a = new int[ battle_group.size() ];
        int count = 0;
        
        ListIterator< Unit > i = battle_group.listIterator();
        while ( i.hasNext() )
        {
            a[ count ] = i.next().getAttackPower();
        }
        return a;
    }
    
    /**
     * 
     * called by player this is only true if the rally point is attacking this
     * turn. this method should return true if the rally point has the attack
     * command at the front of its queue and false otherwise. there is no need
     * for this method to look at the queue. the attack command will set a
     * variable in this object so that it knows if it is attacking. this method
     * should return the value that was set by the attack command.
     */
    public boolean isAttacking()
    {
        return attacking;
    }
    
    /**
     * 
     * called by player this method is what is called when defensive damage is
     * being applied against this rally point army.
     */
    public void takeDamage( int damage )
    {
        // pick random unit in battle group and apply to that.
        Random r = new Random();
        int index = r.nextInt( battle_group.size() );
        battle_group.get( index ).takeDamage( damage );
    }
    
    /**
     * 
     * this will return an array (a list is okay too.) of all the units not in
     * this rallypoint's battlegroup. used by player to get units to move into
     * the location of the rallypoint.
     */
    public Unit[] getReserves()
    {
        return reinforcements.toArray( new Unit[ reinforcements.size() ] );
    }
    
    /**
     * 
     * called by player. this will return an array (a list is fine) of all the
     * units in this rallypoint's battle group.
     */
    public LinkedList< Unit > getBattleGroup()
    {
        return battle_group;
    }
    
    /**
     * 
     * called by player returns true if there are more than zero units in this
     * rally points battle group
     */
    public boolean hasBattleGroup()
    {
        return !battle_group.isEmpty();
    }
    
    /**
     * 
     * called by player. causes the rallypoint battle group to take a step
     * forward (not an actual movement) once enough steps have been taken such
     * that the number of steps is equal to the highest tickspertile of the unit
     * in it, this rallypoint will be moved by the player in the desired
     * direction. you do not have to keep track of the direction the rallypoint
     * is moving. player will do that. (that information is contained in the
     * command.) this method will only return true when enough steps have been
     * taken.
     * 
     * Also changes the facing of the rally point to the directing in which it
     * is moving.
     * 
     * @param dir
     *            the direction in which the rally point is moving.
     */
    public boolean step( Direction dir )
    {
        direction_facing = dir;
        attacking = false;
        defending = false;
        
        System.out.println( "Rally Point Step called." );
        if ( stepcount >= max_speed - 1 )
        {
            stepcount = 0;
            return true;
        }
        else
        {
            stepcount++;
            return false;
        }
    }
    
    /**
     * call this method each time battle group changes in order to update the
     * RallyPoint variable that holds the speed of the slowest unit.
     */
    private void updateMax_speed()
    {
        max_speed = 0;
        int curr_speed; // speed in ticks per tile. (e.g. hours per mile)
        // find highest tickspertile for the unit in army (slowest unit)
        ListIterator< Unit > i = battle_group.listIterator();
        while ( i.hasNext() )
        {
            curr_speed = i.next().getStepsPerTile();
            if ( curr_speed > max_speed )
            {
                max_speed = curr_speed;
            }
        }
    }
    
    /**
     * 
     * called by player. this rallypoint shall move the specified unit from its
     * reserve group to its battle group. if the specified unit is not in this
     * army or is already in the battlegroup then throw it on the ground.
     */
    public void transferUnit( Unit unit )
    {
        try
        {
            // check if specified unit is already in the battle group.
            if ( battle_group.contains( unit ) ) new ThrowItOnTheGround().cuzMyDadIsNotA( this );
            
            reinforcements.remove( unit );
            treinforcements.removeSelectableInstance( unit );
            
            battle_group.add( unit );
            tbattle_group.addSelectableInstance( unit );
        }
        catch ( NoSuchElementException e )
        { // case the unit is not in this army.
            System.out.println( "Tried to add/remove a unit to/from rally point. unit isn't a part of that rally point. " );
            new ThrowItOnTheGround().cuzMyDadIsNotA( this );
        }
        
        updateMax_speed();
    }
    
    /**
     * 
     * this shall return all the units in the army. called by player. this is
     * for the player to compute upkeep costs.
     */
    public Unit[] getEveryone()
    {
        return units.toArray( new Unit[ units.size() ] );
    }
    
    /**
     * 
     * called by player. this will remove the unit from the army. if the unit is
     * not in this army then throw it on the ground.
     */
    public void removeUnit( Unit unit )
    {
        // change unit recruited status
        try
        {
            
            tunits.removeSelectableInstance( unit );
            units.remove( units.indexOf( unit ) );
            
            unit.leave_army();
        }
        catch ( NoSuchElementException e )
        {
            System.out.println( "Tried to remove a unit from rally point. unit isn't a part of that rally point group. " );
            new ThrowItOnTheGround().cuzMyDadIsNotA( this );
        }
        if ( battle_group.contains( unit ) ) battle_group.remove( battle_group.indexOf( unit ) );
        if ( reinforcements.contains( unit ) )
            reinforcements.remove( battle_group.indexOf( unit ) );
        updateMax_speed();
        
        List< SelectableCommand > addiCommands = unit.getAdditionalCommands();
        removeAddCommands( addiCommands );
    }
    
    /**
     * If no other units in the army make these commands available, remove them
     * from the list of available commands.
     * 
     * @param addiCommands
     *            the additional commands to possibly remove.
     */
    private void removeAddCommands( List< SelectableCommand > addiCommands )
    {
        for ( SelectableCommand sc : addiCommands )
        {
            if ( avail_commands.contains( sc ) )
            {
                boolean commandStillAvailable = false;
                
                for ( Unit u : units )
                {
                    if ( u.getAdditionalCommands().contains( sc ) ) commandStillAvailable = true;
                }
                
                if ( !commandStillAvailable ) avail_commands.remove( sc );
            }
        }
    }
    
    /**
     * 
     * called by player. this will add the unit to the army. if the unit is
     * already in the army then throw it on the ground.
     */
    public void addUnit( Unit unit )
    {
        // check if unit already in army
        if ( !units.contains( unit ) )
        {
            // unit not in army.
            // change unit recruited status
            units.add( unit );
            tunits.addSelectableInstance( unit );
            updateMax_speed();
            unit.recruit();
            reinforcements.add( unit );
            treinforcements.addSelectableInstance( unit );
            
            List< SelectableCommand > additionalComm = unit.getAdditionalCommands();
            
            if ( !additionalComm.isEmpty() ) addMoreCommands( additionalComm );
        }
        else
        // now handle case that unit ALREADY in army
        {
            System.out.println( "Tried to add a unit from rally point. unit isn't a part of that rally point group. " );
            new ThrowItOnTheGround().cuzMyDadIsNotA( this );
        }
        
    }
    
    /**
     * Checks to see if the new commands are already available and, if not, adds
     * them to the list of available commands.
     * 
     * @param commList
     *            the list of commands to possibly add.
     */
    private void addMoreCommands( List< SelectableCommand > commList )
    {
        for ( SelectableCommand sc : commList )
        {
            if ( !avail_commands.contains( sc ) )
            {
                avail_commands.add( sc );
            }
        }
    }
    
    // ***Interface Methods***
    
    // SelectableRallyPoint methods
    // [the army ceases to exist, all units go to standby]
    /**
     * sets all units to standby and removes all reference to units from the
     * rally point. signals the rally point id ready for recycling.
     */
    public void disband()
    {
        // set all units states to standby.
        ListIterator< Unit > i = units.listIterator();
        while ( i.hasNext() )
        {
            Unit u = i.next();
            u.powerDown();
            u.leave_army();
        }
        // remove all references to the units in this rally point.
        units.clear();
        battle_group.clear();
        reinforcements.clear();
        
        for ( Unit u : units )
        {
            tunits.removeSelectableInstance( u );
        }
        for ( Unit u : battle_group )
        {
            tbattle_group.removeSelectableInstance( u );
        }
        for ( Unit u : reinforcements )
        {
            treinforcements.removeSelectableInstance( u );
        }
    }
    
    /**
     * Returns the id number of the rally point.
     * 
     * @author Christopher Dudley
     * 
     * @return the id number of the rally point.
     */
    public int getID()
    {
        return id;
    }
    
    public SelectableCommand[] getQueue()
    {
        SelectableCommand[] sCommandArray = new SelectableCommand[ command_queue.size() ];
        sCommandArray = command_queue.toArray( sCommandArray );
        
        return sCommandArray;
    }
    
    // SelectableInstance Interface methods
    
    /**
     * Gets an array of selectable commands the rally point can add to its
     * queue.
     * 
     * @return an array of the currently selectable command_queue.
     */
    public SelectableCommand[] getcommand_queue()
    {
        SelectableCommand[] zecommand_queue = new SelectableCommand[ avail_commands.size() ];
        zecommand_queue = avail_commands.toArray( zecommand_queue );
        return zecommand_queue;
    }
    
    /**
     * Adds the selected command to the queue.
     * 
     * @author Christopher Dudley
     * 
     * @param command
     *            the commmand to be added to the queue.
     */
    public void addCommandToQueue( SelectableCommand command )
    {
        command_queue.add( command.getCommand() );
    }
    
    /**
     * Returns the name of the army that this rally point represents.
     * 
     * @author Christopher Dudley
     * 
     * @return the name of the army the rally point represents.
     */
    public String getName()
    {
        return "Rally Point ";
    }
    
    /**
     * Returns an array of objects containing the type and health of the units
     * in the army.
     * 
     * @author Christopher Dudley
     * 
     * @return instance type and health of units in the army.
     */
    public InstanceTypeAndHealth[] getStatus()
    {
        InstanceTypeAndHealth[] itaha = new InstanceTypeAndHealthClass[ units.size() ];
        ListIterator< Unit > unitIterator = units.listIterator();
        
        int i = 0;
        while ( unitIterator.hasNext() )
        {
            itaha[ i++ ] = ((unitIterator.next()).getStatus())[ 0 ];
        }
        
        return itaha;
    }
    
    /**
     * Cancels all pending orders associated with the rally point.
     */
    public void cancelPendingOrders()
    {
        command_queue.clear();
        command_queue.add( new Wait() );
    }
    
    // ModelInstance interface methods
    /**
     * Don't call this method. It is meaningless for RallyPoint.
     * 
     * @return a runtime exception.
     * @deprecated
     */
    public Map< ResourceType, Integer > getUpkeep()
    {
        throw new RuntimeException( "Don't call getUpkeep on rally point!" );
    }
    
    /**
     * removes the front command on the queue.
     */
    public void popCommand()
    {
        command_queue.pop();
        
        if ( command_queue.isEmpty() ) command_queue.add( new Wait() );
    }
    
    /**
     * clears the rally point's command queue.
     * 
     * @deprecated does the same thing as cancelPendingOrders
     */
    public void clearQueue()
    {
        command_queue.clear();
        command_queue.add( new Wait() );
    }
    
    /**
     * increments power state of each unit in RP by one. must be called 10x in
     * order to put the units to poweredUp state.
     */
    public void powerUp()
    {
        ListIterator< Unit > i = units.listIterator();
        boolean allPoweredUp = true;
        while ( i.hasNext() )
        {
            Unit u = i.next();
            u.powerUp();
            if(!u.isPowered())
                allPoweredUp = false;
        }
        
        if(allPoweredUp)
            popCommand();

    }
    
    /**
     * instantly takes all the units in RP to standby.
     */
    public void powerDown()
    {
        ListIterator< Unit > i = units.listIterator();
        while ( i.hasNext() )
        {
            i.next().powerDown();
        }
        popCommand();
    }
    
    /**
     * if all units are powered, returns true. else, false.
     */
    public boolean isPowered()
    {
        ListIterator< Unit > i = units.listIterator();
        while ( i.hasNext() )
        {
            Unit u = i.next();
            if ( !u.isPowered() ) return false;
        }
        return true;
    }
    
    /**
     * remove all references to units.
     */
    public void decommission()
    {
        for ( Unit u : units )
        {
            tunits.removeSelectableInstance( u );
        }
        for ( Unit u : battle_group )
        {
            tbattle_group.removeSelectableInstance( u );
        }
        for ( Unit u : reinforcements )
        {
            treinforcements.removeSelectableInstance( u );
        }
        units.clear();
        battle_group.clear();
        reinforcements.clear();
    }
    
    /**
     * @return the front command on the queue.
     */
    public Command onTick( Player player )
    {
        return command_queue.getFirst();
    }
    
    /**
     * 
     * @param attacking
     *            is true if RallyPoint is attacking
     */
    public void setAttacking( boolean attacking )
    {
        this.attacking = attacking;
    }
    
    /**
     * 
     * @param defending
     *            is true if RallyPoint is defending
     */
    public void setDefending( boolean defending )
    {
        this.defending = defending;
    }
    
    // Designer added methods
    public void storeTypes( Type battle_group, Type reinforcements, Type everyone )
    {
        this.tbattle_group = battle_group;
        this.treinforcements = reinforcements;
        this.tunits = everyone;
    }
    
    public SelectableCommand[] getCommands()
    {
        SelectableCommand[] zeCommands = new SelectableCommand[ avail_commands.size() ];
        zeCommands = avail_commands.toArray( zeCommands );
        return zeCommands;
    }
    
    public void heal( int amount )
    {
        
        for ( Iterator< Unit > it = battle_group.iterator(); it.hasNext(); )
        {
            Unit healMe = it.next();
            healMe.heal( amount );
        }
    }
}
